/*
 * Copyright 2016 The Bazel Authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.idea.blaze.base.lang.buildfile.lexer;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.Stack;
import java.util.function.Predicate;
import javax.annotation.Nullable;

/**
 * A tokenizer for the BUILD language.
 *
 * <p>Copied from blaze/bazel's lexer. The differences are: 1. Blaze's lexer isn't 'faithful', in
 * that it reorders characters, skips characters, and adds ghost characters. We can't do that,
 * because we need to match the editor's view of the document. 2. Blaze's lexer only lexes entire
 * files (it can't incrementally lex part of a file, starting from a given indent stack depth).
 */
public class BuildLexerBase {

  /**
   * When tokenizing for the purposes of parsing, we handle indentation.<br>
   * For syntax highlighting, we need to tokenize every character, and don't care about indentation.
   */
  public enum LexerMode {
    Parsing,
    SyntaxHighlighting
  }

  // Characters that can come immediately prior to an '=' character to generate
  // a different token
  private static final ImmutableMap<Character, TokenKind> EQUAL_TOKENS =
      ImmutableMap.<Character, TokenKind>builder()
          .put('=', TokenKind.EQUALS_EQUALS)
          .put('!', TokenKind.NOT_EQUALS)
          .put('>', TokenKind.GREATER_EQUALS)
          .put('<', TokenKind.LESS_EQUALS)
          .put('+', TokenKind.PLUS_EQUALS)
          .put('-', TokenKind.MINUS_EQUALS)
          .put('*', TokenKind.STAR_EQUALS)
          .put('/', TokenKind.SLASH_EQUALS)
          .put('%', TokenKind.PERCENT_EQUALS)
          .put('^', TokenKind.CARET_EQUALS)
          .put('&', TokenKind.AMPERSAND_EQUALS)
          .put('|', TokenKind.PIPE_EQUALS)
          .build();

  private final LexerMode mode;

  // Input buffer and position
  private final char[] buffer;
  private int pos;

  private final List<Token> tokens;

  // The number of unclosed open-parens ("(", '{', '[') at the current point in
  // the stream. Whitespace is handled differently when this is nonzero.
  private int openParenStackDepth = 0;

  // The stack of enclosing indentation levels; always contains '0' at the
  // bottom.
  private final Stack<Integer> indentStack = new Stack<>();

  private boolean containsErrors;

  /**
   * Constructs a lexer which tokenizes the contents of the specified InputBuffer. Any errors during
   * lexing are reported on "handler".
   */
  public BuildLexerBase(CharSequence input, int initialStackDepth, LexerMode mode) {
    this.buffer = input.toString().toCharArray();
    // Empirical measurements show roughly 1 token per 8 characters in buffer.
    this.tokens = Lists.newArrayListWithExpectedSize(buffer.length / 8);
    this.pos = 0;
    this.openParenStackDepth = initialStackDepth;
    this.mode = mode;

    indentStack.push(0);
    tokenize();
  }

  /** The number of unclosed open-parens ("(", '{', '[') at the end of this string. */
  public int getOpenParenStackDepth() {
    return openParenStackDepth;
  }

  /**
   * Returns true if there were errors during scanning of this input file or string. The
   * BuildLexerBase may attempt to recover from errors, but clients should not rely on the results
   * of scanning if this flag is set.
   */
  public boolean containsErrors() {
    return containsErrors;
  }

  /** Returns the (mutable) list of tokens generated by the BuildLexerBase. */
  public List<Token> getTokens() {
    return tokens;
  }

  private void popParen() {
    if (openParenStackDepth == 0) {
      error("indentation error");
    } else {
      openParenStackDepth--;
    }
  }

  private void error(String message) {
    error(message, pos - 1, pos - 1);
  }

  protected void error(String message, int start, int end) {
    this.containsErrors = true;
  }

  /** invariant: symbol positions are half-open intervals. */
  private void addToken(TokenKind kind, int left, int right) {
    addToken(kind, left, right, null);
  }

  private void addToken(TokenKind kind, int left, int right, @Nullable Object value) {
    tokens.add(new Token(kind, left, right, value));
  }

  /**
   * Parses an end-of-line sequence, handling statement indentation correctly.
   *
   * <p>UNIX newlines are assumed (LF). Carriage returns are always ignored.
   *
   * <p>ON ENTRY: 'pos' is the index of the char after '\n'. ON EXIT: 'pos' is the index of the next
   * non-space char after '\n'.
   */
  protected void newline() {
    if (mode == LexerMode.SyntaxHighlighting) {
      addToken(TokenKind.NEWLINE, pos - 1, pos);
      return;
    }
    if (openParenStackDepth > 0) {
      newlineInsideExpression(); // in an expression: ignore space
    } else {
      newlineOutsideExpression(); // generate NEWLINE/INDENT/DEDENT tokens
    }
  }

  private void newlineInsideExpression() {
    int oldPos = pos - 1;
    while (pos < buffer.length) {
      switch (buffer[pos]) {
        case ' ':
        case '\t':
        case '\r':
          pos++;
          break;
        default:
          // ignored by the parser
          addToken(TokenKind.WHITESPACE, oldPos, pos);
          return;
      }
    }
    addToken(TokenKind.WHITESPACE, oldPos, pos);
  }

  /**
   * Handle INDENT and DEDENT within statements.
   *
   * <p>Note these tokens have zero length -- this is because we can have an arbitrary number of
   * dedents squeezed into some number of characters, and the size of all the lexical elements must
   * match the number of characters in the file.
   */
  private void newlineOutsideExpression() {
    int oldPos = pos - 1;
    if (pos > 1) { // skip over newline at start of file
      addToken(TokenKind.NEWLINE, oldPos, pos);
      oldPos = pos;
    }

    // we're in a stmt: suck up space at beginning of next line
    int indentLen = 0;
    while (pos < buffer.length) {
      char c = buffer[pos];
      if (c == ' ') {
        indentLen++;
        pos++;
      } else if (c == '\t') {
        indentLen += 8 - indentLen % 8;
        pos++;
      } else if (c == '\n') { // entirely blank line: ignore
        indentLen = 0;
        pos++;
      } else if (c == '#') { // line containing only indented comment
        if (oldPos != pos) {
          addToken(TokenKind.WHITESPACE, oldPos, pos);
          oldPos = pos;
        }
        while (pos < buffer.length && c != '\n') {
          c = buffer[pos++];
        }
        addToken(TokenKind.COMMENT, oldPos, pos - 1, bufferSlice(oldPos, pos - 1));
        oldPos = pos - 1;
        indentLen = 0;
      } else { // printing character
        break;
      }
    }

    if (oldPos != pos) {
      addToken(TokenKind.WHITESPACE, oldPos, pos);
    }
    if (pos == buffer.length) {
      indentLen = 0;
    } // trailing space on last line

    int peekedIndent = indentStack.peek();
    if (peekedIndent < indentLen) { // push a level
      indentStack.push(indentLen);
      addToken(TokenKind.INDENT, pos, pos);

    } else if (peekedIndent > indentLen) { // pop one or more levels
      while (peekedIndent > indentLen) {
        indentStack.pop();
        addToken(TokenKind.DEDENT, pos, pos);
        peekedIndent = indentStack.peek();
      }

      if (peekedIndent < indentLen) {
        error("indentation error");
      }
    }
  }

  /** Collapse adjacent whitespace characters into a single token */
  private void addWhitespace() {
    int oldPos = pos - 1;
    while (pos < buffer.length) {
      switch (buffer[pos]) {
        case ' ':
        case '\t':
        case '\r':
          pos++;
          break;
        default:
          addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
          return;
      }
    }
    addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
  }

  /**
   * Returns true if current position is in the middle of a triple quote delimiter (3 x quot), and
   * advances 'pos' by two if so.
   */
  private boolean skipTripleQuote(char quot) {
    if (pos + 1 < buffer.length && buffer[pos] == quot && buffer[pos + 1] == quot) {
      pos += 2;
      return true;
    } else {
      return false;
    }
  }

  /**
   * Scans a string literal delimited by 'quot', containing escape sequences.
   *
   * <p>ON ENTRY: 'pos' is 1 + the index of the first delimiter ON EXIT: 'pos' is 1 + the index of
   * the last delimiter.
   */
  private void escapedStringLiteral(char quot, boolean isRaw) {
    int oldPos = isRaw ? pos - 2 : pos - 1;
    boolean inTripleQuote = skipTripleQuote(quot);

    // more expensive second choice that expands escaped into a buffer
    StringBuilder literal = new StringBuilder();
    while (pos < buffer.length) {
      char c = buffer[pos];
      pos++;
      switch (c) {
        case '\n':
          if (inTripleQuote) {
            literal.append(c);
            break;
          } else {
            error("unterminated string literal at eol", oldPos, pos);
            addToken(TokenKind.STRING, oldPos, pos - 1, literal.toString());
            newline();
            return;
          }
        case '\\':
          if (pos == buffer.length) {
            error("unterminated string literal at eof", oldPos, pos);
            addToken(TokenKind.STRING, oldPos, pos - 1, literal.toString());
            return;
          }
          if (isRaw) {
            // Insert \ and the following character.
            // As in Python, it means that a raw string can never end with a single \.
            literal.append('\\');
            literal.append(buffer[pos]);
            pos++;
            break;
          }
          c = buffer[pos];
          pos++;
          switch (c) {
            case '\n':
              // ignore end of line character
              break;
            case 'n':
              literal.append('\n');
              break;
            case 'r':
              literal.append('\r');
              break;
            case 't':
              literal.append('\t');
              break;
            case '\\':
              literal.append('\\');
              break;
            case '\'':
              literal.append('\'');
              break;
            case '"':
              literal.append('"');
              break;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
              { // octal escape
                int octal = c - '0';
                if (pos < buffer.length) {
                  c = buffer[pos];
                  if (c >= '0' && c <= '7') {
                    pos++;
                    octal = (octal << 3) | (c - '0');
                    if (pos < buffer.length) {
                      c = buffer[pos];
                      if (c >= '0' && c <= '7') {
                        pos++;
                        octal = (octal << 3) | (c - '0');
                      }
                    }
                  }
                }
                literal.append((char) (octal & 0xff));
                break;
              }
            case 'a':
            case 'b':
            case 'f':
            case 'N':
            case 'u':
            case 'U':
            case 'v':
            case 'x':
              // exists in Python but not implemented in Blaze => error
              error("escape sequence not implemented: \\" + c, oldPos, pos);
              break;
            default:
              // unknown char escape => "\literal"
              literal.append('\\');
              literal.append(c);
              break;
          }
          break;
        case '\'':
        case '"':
          if (c != quot || (inTripleQuote && !skipTripleQuote(quot))) {
            // Non-matching quote, treat it like a regular char.
            literal.append(c);
          } else {
            // Matching close-delimiter, all done.
            addToken(TokenKind.STRING, oldPos, pos, literal.toString());
            return;
          }
          break;
        default:
          literal.append(c);
          break;
      }
    }
    error("unterminated string literal at eof", oldPos, pos);
    addToken(TokenKind.STRING, oldPos, pos, literal.toString());
  }

  /**
   * Scans a string literal delimited by 'quot'.
   *
   * <ul>
   *   <li>ON ENTRY: 'pos' is 1 + the index of the first delimiter
   *   <li>ON EXIT: 'pos' is 1 + the index of the last delimiter.
   * </ul>
   *
   * @param isRaw if true, do not escape the string.
   */
  private void addStringLiteral(char quot, boolean isRaw) {
    int oldPos = isRaw ? pos - 2 : pos - 1;
    int start = pos;

    // Don't even attempt to parse triple-quotes here.
    if (skipTripleQuote(quot)) {
      pos -= 2;
      escapedStringLiteral(quot, isRaw);
      return;
    }

    // first quick optimistic scan for a simple non-escaped string
    while (pos < buffer.length) {
      char c = buffer[pos++];
      switch (c) {
        case '\n':
          error("unterminated string literal at eol", oldPos, pos);
          addToken(TokenKind.STRING, oldPos, pos - 1, bufferSlice(start, pos - 1));
          newline();
          return;
        case '\\':
          if (isRaw) {
            // skip the next character
            pos++;
            break;
          }
          // oops, hit an escape, need to start over & build a new string buffer
          pos = oldPos + 1;
          escapedStringLiteral(quot, false);
          return;
        case '\'':
        case '"':
          if (c == quot) {
            // close-quote, all done.
            addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos - 1));
            return;
          }
      }
    }

    error("unterminated string literal at eof", oldPos, pos);
    addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos));
  }

  private static final ImmutableMap<String, TokenKind> KEYWORD_MAP = createKeywordMap();

  private static ImmutableMap<String, TokenKind> createKeywordMap() {
    ImmutableMap.Builder<String, TokenKind> builder = ImmutableMap.builder();
    for (TokenKind kind : TokenKind.KEYWORDS) {
      builder.put(kind.toString(), kind);
    }
    return builder.build();
  }

  private TokenKind getTokenKindForIdentfier(String id) {
    TokenKind kind = KEYWORD_MAP.get(id);
    return kind == null ? TokenKind.IDENTIFIER : kind;
  }

  private String scanIdentifier() {
    int oldPos = pos - 1;
    while (pos < buffer.length) {
      switch (buffer[pos]) {
        case '_':
        case 'a':
        case 'b':
        case 'c':
        case 'd':
        case 'e':
        case 'f':
        case 'g':
        case 'h':
        case 'i':
        case 'j':
        case 'k':
        case 'l':
        case 'm':
        case 'n':
        case 'o':
        case 'p':
        case 'q':
        case 'r':
        case 's':
        case 't':
        case 'u':
        case 'v':
        case 'w':
        case 'x':
        case 'y':
        case 'z':
        case 'A':
        case 'B':
        case 'C':
        case 'D':
        case 'E':
        case 'F':
        case 'G':
        case 'H':
        case 'I':
        case 'J':
        case 'K':
        case 'L':
        case 'M':
        case 'N':
        case 'O':
        case 'P':
        case 'Q':
        case 'R':
        case 'S':
        case 'T':
        case 'U':
        case 'V':
        case 'W':
        case 'X':
        case 'Y':
        case 'Z':
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
          pos++;
          break;
        default:
          return bufferSlice(oldPos, pos);
      }
    }
    return bufferSlice(oldPos, pos);
  }

  /**
   * Scans an identifier or keyword.
   *
   * <p>ON ENTRY: 'pos' is 1 + the index of the first char in the identifier. ON EXIT: 'pos' is 1 +
   * the index of the last char in the identifier.
   *
   * @return the identifier or keyword token.
   */
  private void addIdentifierOrKeyword() {
    int oldPos = pos - 1;
    String id = scanIdentifier();
    TokenKind kind = getTokenKindForIdentfier(id);
    addToken(kind, oldPos, pos, (kind == TokenKind.IDENTIFIER) ? id : null);
  }

  private String scanDecimal() {
    int oldPos = pos;
    while (pos < buffer.length) {
      char c = buffer[pos];
      switch (c) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
          pos++;
          break;
        default:
          return bufferSlice(oldPos, pos);
      }
    }
    return bufferSlice(oldPos, pos);
  }

  private String scanInteger() {
    int oldPos = pos;
    while (pos < buffer.length) {
      char c = buffer[pos];
      switch (c) {
        case 'X':
        case 'x': // for hexadecimal prefix
        case 'O':
        case 'o': // for octal prefix
        case 'a':
        case 'A':
        case 'b':
        case 'B':
        case 'c':
        case 'C':
        case 'd':
        case 'D':
        case 'e':
        case 'E':
        case 'f':
        case 'F':
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
          pos++;
          break;
        default:
          return bufferSlice(oldPos, pos);
      }
    }
    return bufferSlice(oldPos, pos);
  }

  /**
   * Scans an integer or float literal.
   *
   * <p>ON ENTRY: 'pos' is the index of the first char in the literal. ON EXIT: 'pos' is 1 + the
   * index of the last char in the literal.
   */
  private void addNumber() {
    // https://github.com/bazelbuild/starlark/blob/master/spec.md#lexical-elements

    int oldPos = pos;
    String literal = scanInteger();

    String substring;
    int radix;
    boolean isFloat = false;
    boolean hasErrors = false;
    if (literal.startsWith("0x") || literal.startsWith("0X")) {
      radix = 16;
      substring = literal.substring(2);
    } else if (literal.startsWith("0o") || literal.startsWith("0O")) {
      radix = 8;
      substring = literal.substring(2);
    } else if (literal.startsWith("0") && literal.length() > 1) {
      radix = 8;
      substring = literal.substring(1);
      error("invalid octal value `" + literal + "`, should be: `0o" + substring + "`");
    } else {
      radix = 10;
      substring = literal;
    }

    if (radix == 10 && lookaheadIs(0, '.')) {
      pos++;
      isFloat = true;
      scanDecimal();
      literal = bufferSlice(oldPos, pos);
      substring = literal;
    }
    if (radix == 10 && (lookaheadIs(0, 'e') || lookaheadIs(0, 'E'))) {
      pos++;
      isFloat = true;
      if (lookaheadIs(0, '+') || lookaheadIs(0, '-')) {
        pos++;
      }
      String exp = scanDecimal();
      if (exp.isEmpty()) {
        hasErrors = true;
      }
      literal = bufferSlice(oldPos, pos);
      substring = literal;
    }

    if (isFloat) {
      double value = 0;
      try {
        value = Double.parseDouble(substring);
      } catch (NumberFormatException e) {
        hasErrors = true;
      }
      if (hasErrors) {
        error("invalid float constant: " + literal, oldPos, pos);
      }
      addToken(TokenKind.FLOAT, oldPos, pos, value);
    } else {
      int value = 0;
      try {
        value = Integer.parseInt(substring, radix);
      } catch (NumberFormatException e) {
        hasErrors = true;
      }
      if (hasErrors) {
        error("invalid base-" + radix + " integer constant: " + literal);
      }
      addToken(TokenKind.INT, oldPos, pos, value);
    }
  }

  /**
   * Tokenizes a two-char operator.
   *
   * @return true if it tokenized an operator
   */
  private boolean tokenizeTwoChars() {
    if (pos + 2 >= buffer.length) {
      return false;
    }
    char c1 = buffer[pos];
    char c2 = buffer[pos + 1];
    TokenKind tok = null;
    if (c2 == '=') {
      tok = EQUAL_TOKENS.get(c1);
    } else if (c2 == '*' && c1 == '*') {
      tok = TokenKind.STAR_STAR;
    }
    if (tok == null) {
      return false;
    }
    addToken(tok, pos, pos + 2);
    return true;
  }

  /** Test if the character at pos+p is c. */
  private boolean lookaheadIs(int p, char c) {
    return pos + p < buffer.length && buffer[pos + p] == c;
  }

  private boolean lookaheadMatches(int p, Predicate<Character> pred) {
    return pos + p < buffer.length && pred.test(buffer[pos + p]);
  }

  /** Performs tokenization of the character buffer of file contents provided to the constructor. */
  private void tokenize() {
    while (pos < buffer.length) {
      if (tokenizeTwoChars()) {
        pos += 2;
        continue;
      }
      char c = buffer[pos];
      pos++;
      switch (c) {
        case '{':
          addToken(TokenKind.LBRACE, pos - 1, pos);
          openParenStackDepth++;
          break;
        case '}':
          addToken(TokenKind.RBRACE, pos - 1, pos);
          popParen();
          break;
        case '(':
          addToken(TokenKind.LPAREN, pos - 1, pos);
          openParenStackDepth++;
          break;
        case ')':
          addToken(TokenKind.RPAREN, pos - 1, pos);
          popParen();
          break;
        case '[':
          addToken(TokenKind.LBRACKET, pos - 1, pos);
          openParenStackDepth++;
          break;
        case ']':
          addToken(TokenKind.RBRACKET, pos - 1, pos);
          popParen();
          break;
        case '>':
          if (lookaheadIs(0, '>') && lookaheadIs(1, '=')) {
            addToken(TokenKind.GREATER_GREATER_EQUALS, pos - 1, pos + 2);
            pos += 2;
          } else if (lookaheadIs(0, '>')) {
            addToken(TokenKind.GREATER_GREATER, pos - 1, pos + 1);
            pos++;
          } else {
            // >= is handled by tokenizeTwoChars.
            addToken(TokenKind.GREATER, pos - 1, pos);
          }
          break;
        case '<':
          if (lookaheadIs(0, '<') && lookaheadIs(1, '=')) {
            addToken(TokenKind.LESS_LESS_EQUALS, pos - 1, pos + 2);
            pos += 2;
          } else if (lookaheadIs(0, '<')) {
            addToken(TokenKind.LESS_LESS, pos - 1, pos + 1);
            pos++;
          } else {
            // <= is handled by tokenizeTwoChars.
            addToken(TokenKind.LESS, pos - 1, pos);
          }
          break;
        case ':':
          addToken(TokenKind.COLON, pos - 1, pos);
          break;
        case ',':
          addToken(TokenKind.COMMA, pos - 1, pos);
          break;
        case '+':
          addToken(TokenKind.PLUS, pos - 1, pos);
          break;
        case '-':
          addToken(TokenKind.MINUS, pos - 1, pos);
          break;
        case '|':
          addToken(TokenKind.PIPE, pos - 1, pos);
          break;
        case '^':
          addToken(TokenKind.CARET, pos - 1, pos);
          break;
        case '&':
          addToken(TokenKind.AMPERSAND, pos - 1, pos);
          break;
        case '=':
          addToken(TokenKind.EQUALS, pos - 1, pos);
          break;
        case '%':
          addToken(TokenKind.PERCENT, pos - 1, pos);
          break;
        case '/':
          if (lookaheadIs(0, '/') && lookaheadIs(1, '=')) {
            addToken(TokenKind.SLASH_SLASH_EQUALS, pos - 1, pos + 2);
            pos += 2;
          } else if (lookaheadIs(0, '/')) {
            addToken(TokenKind.SLASH_SLASH, pos - 1, pos + 1);
            pos++;
          } else {
            // /= is handled by tokenizeTwoChars.
            addToken(TokenKind.SLASH, pos - 1, pos);
          }
          break;
        case ';':
          addToken(TokenKind.SEMI, pos - 1, pos);
          break;
        case '*':
          addToken(TokenKind.STAR, pos - 1, pos);
          break;
        case ' ':
        case '\t':
        case '\r':
          addWhitespace();
          break;
        case '\\':
          // Backslash character is valid only at the end of a line (or in a string)
          if (lookaheadIs(0, '\n')) {
            // treat end of line backslash and newline char as whitespace
            // (they're ignored by the parser)
            pos++;
            addToken(TokenKind.WHITESPACE, pos - 2, pos, Character.toString(c));
          } else {
            addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
          }
          break;
        case '\n':
          newline();
          break;
        case '#':
          int oldPos = pos - 1;
          while (pos < buffer.length) {
            c = buffer[pos];
            if (c == '\n') {
              break;
            } else {
              pos++;
            }
          }
          addToken(TokenKind.COMMENT, oldPos, pos, bufferSlice(oldPos, pos));
          break;
        case '\'':
        case '\"':
          addStringLiteral(c, false);
          break;
        default:
          // detect raw strings, e.g. r"str"
          if (c == 'r' && (lookaheadIs(0, '\'') || lookaheadIs(0, '\"'))) {
            c = buffer[pos];
            pos++;
            addStringLiteral(c, true);
            break;
          }

          // Distinguish dot vs. start of a float.
          if (c == '.' && !lookaheadMatches(0, Character::isDigit)) {
            addToken(TokenKind.DOT, pos - 1, pos);
          } else if (Character.isDigit(c) || c == '.') {
            pos--;
            addNumber();
          } else if (Character.isJavaIdentifierStart(c) && c != '$') {
            addIdentifierOrKeyword();
          } else {
            // Some characters in Python are not recognized in Blaze syntax (e.g. '!')
            addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
            error("invalid character: '" + c + "'");
          }
          break;
      } // switch
    } // while
  }

  /**
   * Returns parts of the source buffer based on offsets
   *
   * @param start the beginning offset for the slice
   * @param end the offset immediately following the slice
   * @return the text at offset start with length end - start
   */
  private String bufferSlice(int start, int end) {
    return new String(this.buffer, start, end - start);
  }
}
